package abc.config;

import abc.handler.RefreshScope;
import abc.utils.KillPortUtil;
import abc.utils.NetworkUtil;
import abc.utils.StringUtil;
import abc.utils.YamlUtil;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.naming.NacosNamingService;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLParser;
import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import jakarta.annotation.Resource;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;

@Data
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "spring.cloud.nacos", name = "enabled", havingValue = "true")
public class NacosStarterConfig implements EnvironmentPostProcessor,
        WebServerFactoryCustomizer<NettyReactiveWebServerFactory>, ApplicationContextAware,
        ApplicationListener<ContextRefreshedEvent> {

    private static final String ENCODING = "UTF-8";

    private static int useServerPort = 0;
    private static int useServerSSLPort = 0;
    private static String useServerPorts = "";

    @Resource
    protected NacosConfigProperty nacosConfigProperty;

    //当前项目环境
    @Value("${springProfilesActive}")
    private String active;

    //当前服务名
    @Value("${serviceName}")
    private String serverName;

    //当前Nacos分组
    @Value("${springCloudNacosDiscoveryGoup}")
    private String groupName;

    //当前Nacos命名空间
    @Value("${springCloudNacosDiscoveryNamespace}")
    private String namespace;

    //是否关闭自动注册服务
    @Value("${springCloudNacosDiscoveryRegisterEnabled}")
    private Boolean discoveryEnabled;

    //当前Nacos连接地址
    @Value("${springCloudNacosDiscoveryServerAddr}")
    private String discoveryServerAddr;

    @Value("${springCloudNacosConfigServerAddr}")
    private String configServerAddr;

    @Value("${springCloudNacosDiscoveryIp}")
    private String ip;

    @Value("${springCloudNacosDiscoveryPort}")
    private String discoveryPort;

    @Value("${serverPort}")
    private String port;

    @Value("${serverPorts}")
    private String ports;

    @Value("${springCloudNacosConfigSharedConfigs}")
    private String sharedConfigs;

    @NacosInjected
    private NamingService namingService;

    public static final String CONFIG_KEY = "bootstrapProperties-${application!}.yml";

    public static ConfigService configService;

    /**
     * @Date: 2022/09/09
     * @Description:注册服务
     */
    public void registerNacosServer(Instance instance) throws Exception {
        try {
            if(namingService == null) {
                namingService = new NacosNamingService(discoveryServerAddr);
            }
            namingService.registerInstance(instance.getServiceName(), groupName, instance);
            log.info("Nacos server register details:["+instance.getIp()+"/" + groupName + "/" + instance.getServiceName() + "/" + namespace + "]！");
        } catch (Exception e) {
            throw new Exception("ServerName:[" + instance.getServiceName() + "] register faild: " + e.getMessage());
        }
    }


    /**
     * @Date: 2022/09/09
     * @Description:注册服务
     */
    public void registerNacosServer() {
        try {
            Instance instance = new Instance();
            //获取配置文件中的 Nacos服务发现IP
            instance.setIp(getClientIp());
            instance.setServiceName(serverName);
            instance.setPort(getDynamicPort());
            log.info("server details: " + instance);
            registerNacosServer(instance);
        } catch (Exception e) {
            log.error("Nacos server register faild: " + e.getMessage());
        }
    }

    private String getClientIp() {
        if(StringUtil.isNotEmpty(this.ip)) {
            return this.ip;
        }

        this.ip = "127.0.0.1";
        //是否未配置  Nacos服务发现IP
        //根据环境环境 获取当前服务 IP地址
        String clientIp = NetworkUtil.getInternetIp();
        if(StringUtil.isNotEmpty(clientIp)) {
            this.ip = clientIp;
            return this.ip;
        }
        clientIp = NetworkUtil.getPublicIp();
        if(StringUtil.isNotEmpty(clientIp)) {
            this.ip = clientIp;
            return this.ip;
        }
        if(StringUtil.isNotEmpty(discoveryServerAddr)) {
            this.ip = StringUtil.splitString2List(discoveryServerAddr, ":").get(0);
            return this.ip;
        }
        if(StringUtil.isNotEmpty(configServerAddr)) {
            this.ip = StringUtil.splitString2List(discoveryServerAddr, ":").get(0);
            return this.ip;
        }

        return this.ip;
    }


    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication springApplication) {
        init();
        if(discoveryEnabled==null || !discoveryEnabled) {
            return;
        }
        Properties properties = new Properties();
        Properties configProperties = new Properties();
        properties.put("serverAddr", discoveryServerAddr);
        if(nacosConfigProperty == null) {
            nacosConfigProperty = new NacosConfigProperty();
        }
        if(sharedConfigs == null) {
            return;
        }
        if(nacosConfigProperty.getSharedConfigs() == null) {
            sharedConfigs = sharedConfigs.replaceAll("^\\[(.+)\\]$", "$1");
            if(StringUtil.isNotEmpty(sharedConfigs)) {
                nacosConfigProperty.setSharedConfigs(StringUtil.splitString2List(sharedConfigs, ","));
            }
            else {
                nacosConfigProperty.setSharedConfigs(new ArrayList<>());
            }
        }
        nacosConfigProperty.getSharedConfigs().forEach(dataId-> {
            try {
                // 获取配置
                configService = NacosFactory.createConfigService(properties);
                String configText = configService.getConfig(dataId, groupName, 5000);
                if(StringUtil.isEmpty(configText)) {
                    log.error("Fail to load config from nacos, retry again!!!");
                    log.error("dataId=" + dataId);
                    return;
                }
                mergePropertys(loadPropertiesByYaml(new ByteArrayInputStream(configText.getBytes(StandardCharsets.UTF_8))), configProperties);

            } catch (NacosException e) {
                log.error(e.getMessage(), e);
            }
        });

        if(configProperties.isEmpty()) {
            log.error("Fail to load bootstrap config!!");
            return;
        }
        MutablePropertySources propertySources = environment.getPropertySources();

        //以外部配置文件为准
        propertySources.addFirst(new PropertiesPropertySource(CONFIG_KEY, configProperties));
        //以application.properties文件为准
        //propertySources.addLast(new PropertiesPropertySource(CONFIG_KEY, configProperties));
    }

    private void init() {
        try {
            if(discoveryEnabled==null || !discoveryEnabled) {
                return;
            }
            YamlUtil.loadConfig("bootstrap");
            serverName = YamlUtil.get("spring.application.name:${application!}");
            groupName = YamlUtil.get("spring.cloud.nacos.discovery.group:DEFAULT_GROUP");
            namespace = YamlUtil.get("spring.cloud.nacos.discovery.namespace:public");
            discoveryEnabled = (YamlUtil.get("spring.cloud.nacos.discovery.register-enabled", Boolean.class)==null ||
                    YamlUtil.get("spring.cloud.nacos.discovery.register-enabled:true", Boolean.class)==true);
            discoveryServerAddr = YamlUtil.get("spring.cloud.nacos.discovery.server-addr:127.0.0.1");
            configServerAddr = YamlUtil.get("spring.cloud.nacos.config.server-addr:127.0.0.1");
            ip = YamlUtil.get("spring.cloud.nacos.discovery.ip");
            discoveryPort = YamlUtil.get("spring.cloud.nacos.discovery.port");
            port = YamlUtil.get("server.port:");
            ports = YamlUtil.get("server.ports:");
            sharedConfigs = YamlUtil.get("spring.cloud.nacos.config.shared-configs");

        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    private void mergePropertys(Properties srcProperties, Properties targetProperties) {
        if(StringUtil.isNotEmpty(srcProperties.getProperty("server.port"))) {
            this.port = srcProperties.getProperty("server.port");
        }
        if(StringUtil.isNotEmpty(srcProperties.getProperty("server.ports"))) {
            this.ports = srcProperties.getProperty("server.ports");
        }
        for(Object key : srcProperties.keySet()) {
            if(!targetProperties.containsKey(key)) {
                targetProperties.put(key, srcProperties.get(key));
            }
        }
    }

    public static Properties loadPropertiesByYaml(InputStream stream) {
        final String DOT = ".";
        Properties result = new Properties();
        try {
            YAMLFactory yamlFactory = new YAMLFactory();
            YAMLParser parser = yamlFactory.createParser(
                    new InputStreamReader(stream, Charset.forName(ENCODING)));

            String key = "";
            String value = null;
            JsonToken token = parser.nextToken();
            while (token != null) {
                if (JsonToken.START_OBJECT.equals(token)) {
                    // do nothing
                } else if (JsonToken.FIELD_NAME.equals(token)) {
                    if (key.length() > 0) {
                        key = key + DOT;
                    }
                    key = key + parser.getCurrentName();

                    token = parser.nextToken();
                    value = parser.getText();
                    if (JsonToken.START_OBJECT.equals(token)) {
                        continue;
                    }
                    if(JsonToken.START_ARRAY.equals(token)) {
                        result.setProperty(key, value);
                        token = parser.nextToken();
                        while (!JsonToken.END_ARRAY.equals(token)) {
                            if (JsonToken.START_OBJECT.equals(token) ||
                                    JsonToken.END_OBJECT.equals(token)) {
                                value = parser.getText();
                                result.setProperty(key, result.get(key) + value);
                            }
                            else {
                                value = parser.getText();
                                String name = parser.getCurrentName();
                                if(StringUtil.isNotEmpty(name) && !value.equals(name)) {
                                    if(!result.get(key).toString().endsWith("{")) {
                                        result.setProperty(key, result.get(key) + ", ");
                                    }
                                    result.setProperty(key, result.get(key) + String.format("\"%s\":\"%s\"", name, value));
                                }
                                else if(StringUtil.isEmpty(name) && value != null) {
                                    if(result.get(key) instanceof String) {
                                        if(((String) result.get(key)).endsWith("[\"")) {
                                            result.setProperty(key, result.get(key) + String.format("%s\"", value));
                                        }
                                        else if(((String) result.get(key)).endsWith("[")) {
                                            result.setProperty(key, result.get(key) + String.format("\"%s\"", value));
                                        }
                                        else {
                                            result.setProperty(key, result.get(key) + String.format(", \"%s\"", value));
                                        }
                                    }
                                }
                            }
                            token = parser.nextToken();
                        }
                        result.setProperty(key, result.get(key) + "]");
                        if(result.getProperty(key).startsWith("[{") && !result.getProperty(key).startsWith("}]")) {
                            result.setProperty(key, result.getProperty(key).replace("\":\"[", "\":["));
                        }
                    }
                    else {
                        result.setProperty(key, value);
                    }

                    int dotOffset = key.lastIndexOf(DOT);
                    if (dotOffset > 0) {
                        key = key.substring(0, dotOffset);
                    }
                    value = null;
                } else if (JsonToken.END_OBJECT.equals(token)) {
                    int dotOffset = key.lastIndexOf(DOT);
                    if (dotOffset > 0) {
                        key = key.substring(0, dotOffset);
                    } else {
                        key = "";
                    }
                }
                token = parser.nextToken();
            }
            parser.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return result;
    }

    public Integer getDynamicPort() {
        Integer port = 0;
        if(StringUtil.isNotEmpty(discoveryPort)) {
            port = Integer.parseInt(discoveryPort);
            if(port > 0) {
                return port;
            }
        }

        if(StringUtil.isNotEmpty(System.getProperty("server.port"))) {
            useServerPort = Integer.parseInt(System.getProperty("server.port"));
            if(useServerPort > 0) {
                return useServerPort;
            }
        }

        if(StringUtil.isNotEmpty(this.port)) {
            port = Integer.parseInt(this.port);
            if(port > 0) {
                return port;
            }
        }

        if(StringUtil.isNotEmpty(System.getProperty("server.ports"))) {
            ports = System.getProperty("server.ports");
        }
        if (StringUtil.isNotEmpty(ports)) {
            List<Integer> split = StringUtil.splitString2IntList(ports, ",");
            if(Pattern.matches("^\\d+\\-\\d+$", ports)) {
                List<Integer> splitRegion = StringUtil.splitString2IntList(ports, "\\-");
                Integer a = splitRegion.get(0);
                Integer b = splitRegion.get(1);
                if(a>b) {
                    a = splitRegion.get(1);
                    b = splitRegion.get(0);
                }
                for(Integer i=a; i<=b; i++) {
                    Integer p1 = getAvailablePort(i);
                    if (p1>0) return p1;
                }
            }
            else {
                for (Integer p : split) {
                    Integer p1 = getAvailablePort(p);
                    if (p1>0) return p1;
                }
            }
        }
        return 0;
    }

    private Integer getAvailablePort(Integer p) {
        if (isPortAvailable(p)) {
            //环境属性保持一致
            System.getProperties().put("server.port", p);
            /**
             * 如果使用了spring cloud config，那么server.port会被提前替换为yml里面定义的值
             * 此时可以自定义一个dynamicPort
             */
            System.getProperties().put("dynamicPort", ports);
            //设置启动端口
            this.port = p.toString();
            System.out.println("Succeed bind port " + p);
            return p;
        } else {
            System.out.println("The port {} is occupied " + p);
        }
        return 0;
    }

    @Override
    public void customize(NettyReactiveWebServerFactory factory) {
        registerNacosServer();
        factory.setPort(Integer.parseInt(this.port));
        factory.addServerCustomizers(server -> server.tcpConfiguration(tcp ->
                tcp.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 60000)
                        .doOnConnection(connection ->
                        {
                            connection.addHandlerLast(new WriteTimeoutHandler(60000));
                            connection.addHandlerLast(new ReadTimeoutHandler(60000));
                        })));
    }

    private static boolean isPortAvailable(int port) {
        int i = 1;
        while (i <= 3) {
            try {
                checkPortAvailable("0.0.0.0", port);
                String hostAddress = null;
                try {
                    hostAddress = InetAddress.getLocalHost().getHostAddress();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (hostAddress != null) {
                    checkPortAvailable(hostAddress, port);
                }
                return true;
            } catch (Exception e) {
                //第2次尝试干掉被占用的端口
                if (i == 2) {
                    try {
                        /**
                         * 非强制杀死进程，只会杀死TCP:CLOSE_WAIT进程。(单机集群环境下 TCP:LISTEN 的进程显然不允许被杀死)
                         */
                        KillPortUtil.KillResult result = KillPortUtil.kill(false, port);//isForce 一定传false,否则导致正常运行的服务宕机
                        if (KillPortUtil.KillResult.AVAILABLE.equals(result)) {
                            return true;
                        }
                    } catch (IOException e1) {
                        System.out.println(e1.getMessage());
                        System.out.println(e1.toString());
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            } finally {
                i++;
            }
        }
        return false;
    }

    private static void checkPortAvailable(String host, int port) throws Exception {
        Socket s = new Socket();
        s.bind(new InetSocketAddress(host, port));
        s.close();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(configService == null) {
            return;
        }

        nacosConfigProperty.getSharedConfigs().forEach(dataId-> {
            try {
                configService.addListener(dataId, groupName, new Listener() {
                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        log.info("{} 本次修改后的内容: {}", dataId, configInfo);
                        if(StringUtil.isEmpty(configInfo)) {
                            log.error("Fail to load config from nacos, retry again!!!");
                            log.error("dataId=" + dataId);
                        }

                        Properties configProperties = new Properties();
                        if(StringUtil.isEmpty(configInfo)) {
                            return;
                        }

                        //configProperties.load(new StringReader(configInfo));
                        mergePropertys(loadPropertiesByYaml(new ByteArrayInputStream(configInfo.getBytes(StandardCharsets.UTF_8))), configProperties);
                        MutablePropertySources propertySources = ((ConfigurableEnvironment) applicationContext.getEnvironment()).getPropertySources();

                        //以外部配置文件为准
                        propertySources.addFirst(new PropertiesPropertySource(CONFIG_KEY, configProperties));
                        RefreshScope.refresh(configProperties);
                        applicationContext.publishEvent(new ContextRefreshedEvent(applicationContext));
                    }

                    @Override
                    public Executor getExecutor() {
                        return null;
                    }
                });

            } catch (NacosException e) {
                log.error(e.getMessage(), e);
            }
        });
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("ContextRefreshedEvent");
    }
}
